package com.lin.operatorlogrecord.aspect;

import com.lin.operatorlogrecord.entity.annotation.OperatorLogAnnotation;
import com.lin.operatorlogrecord.entity.model.OperatorLog;
import com.lin.operatorlogrecord.exception.OperatorLogException;
import com.lin.operatorlogrecord.mapper.OperatorLogMapper;
import com.lin.operatorlogrecord.strategy.IOperateLogStrategy;
import com.lin.operatorlogrecord.unit.OperatorLogUtil;
import com.lin.operatorlogrecord.unit.TokenUtil;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.time.LocalDateTime;

/**
 * @Author: chenghao.lin
 * @Date: 2023/09/21
 * @Description: 生成操作日志的切面
 */

@Component
@Aspect
@RequiredArgsConstructor
public class OperatorLogAspect {

    private final OperatorLogMapper operatorLogMapper;
    private final ParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();

    /**
     * 切点
     *
     * @param operatorLogAnnotation
     */
    @Pointcut("@annotation(operatorLogAnnotation)")
    public void operatorLogPointCut(OperatorLogAnnotation operatorLogAnnotation) {
    }

    /**
     * 环绕切面，添加操作日志
     */
    @Around(value = "operatorLogPointCut(operatorLogAnnotation)", argNames = "joinPoint,operatorLogAnnotation")
    public Object operatorLogAround(ProceedingJoinPoint joinPoint, OperatorLogAnnotation operatorLogAnnotation) throws Throwable{
        OperatorLog operatorLog = new OperatorLog();
        // 设置服务器开始处理时间
        operatorLog.setStartTime(LocalDateTime.now());
        // 设置策略类型
        operatorLog.setType(operatorLogAnnotation.logType());
        // 设置对象类型
        Class<?>[] tableEntity = operatorLogAnnotation.tableEntity();
        operatorLog.setOperatorObjType(OperatorLogUtil.getObjectTypeEnum(tableEntity[0]));
        // 获取切面方法中的参数内容
        EvaluationContext context = getEvaluationContext(joinPoint);
        // 获取操作对象ID
        String operatorObjId = getOperatorObjIdBySpel(operatorLogAnnotation.operatorObjIdSpel(), context);
        // 获取策略类
        IOperateLogStrategy iOperateLogStrategy;
        try {
            iOperateLogStrategy = operatorLogAnnotation.iOperatorLogStrategy().newInstance();
        } catch (IllegalAccessException | InstantiationException e) {
            throw new OperatorLogException("新建策略实现类失败", e);
        }
        // 设置操作日志内容
        Object requestObj = getRequestObjBySpel(operatorLogAnnotation.requestObjSpel(), context);
        operatorLog.setContent(iOperateLogStrategy.getOperatorContent(requestObj, tableEntity, operatorObjId));
        // 设置当前用户ID
        operatorLog.setUserId(TokenUtil.getCurrentUserId());
        // 执行方法
        Object result;
        try {
            result = joinPoint.proceed();
            // 设置操作对象ID
            setOperatorObjId(operatorLog, operatorObjId, result);
            // 设置操作成功
            operatorLog.setHasFinish(true);
        } catch (Throwable throwable) {
            // 设置操作失败
            operatorLog.setHasFinish(false);
            // 设置失败原因
            operatorLog.setFailReason(throwable.getMessage());
            throw throwable;
        } finally {
            // 设置服务器处理完成时间
            operatorLog.setEndTime(LocalDateTime.now());
            // TODO 可以采用RabbitMQ 保存操作日志
            operatorLogMapper.insert(operatorLog);
        }
        return result;
    }

    /**
     * 设置操作对象ID，分为新增和非新增两种情况
     * @param operatorLog
     * @param operatorObjId
     * @param result
     */
    private void setOperatorObjId(OperatorLog operatorLog, String operatorObjId, Object result) {
        // 若operatorObjId为空，新增操作，从返回结果获取
        if (StringUtils.isBlank(operatorObjId)) {
            if (result instanceof String || result instanceof Integer) {
                operatorLog.setOperatorObjId(result.toString());
            } else {
                throw new OperatorLogException("新增操作要返回新增对象ID");
            }
        // 若operatorObjId非空
        } else {
            operatorLog.setOperatorObjId(operatorObjId);
        }
    }

    /**
     * 通过Spel获取前端请求对象
     * @param requestObjSpel spring表达式
     * @param context  切面方法参数值
     * @return
     */
    private Object getRequestObjBySpel(String requestObjSpel, EvaluationContext context) {
        if (StringUtils.isBlank(requestObjSpel)) {
            return null;
        }
        ExpressionParser parser = new SpelExpressionParser();
        return parser.parseExpression(requestObjSpel).getValue(context, Object.class);
    }

    /**
     * 通过Spel获取操作对象ID
     * @param operatorObjId spring表达式
     * @param context  切面方法参数值
     * @return
     */
    private String getOperatorObjIdBySpel(String operatorObjId, EvaluationContext context) {
        if (StringUtils.isBlank(operatorObjId)) {
            return null;
        }
        ExpressionParser parser = new SpelExpressionParser();
        return parser.parseExpression(operatorObjId).getValue(context, String.class);
    }

    /**
     * 获取切面方法中的参数内容
     * @param joinPoint
     * @return
     */
    private EvaluationContext getEvaluationContext(ProceedingJoinPoint joinPoint) {
        // 获取切面方法参数
        Object[] args = joinPoint.getArgs();
        // 获取切面方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        // 填充方法参数
        EvaluationContext context = new StandardEvaluationContext();
        String[] parameterNames = discoverer.getParameterNames(method);
        for (int i = 0; i < args.length; i++) {
            context.setVariable(parameterNames[i], args[i]);
        }
        return context;
    }


}
